home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / frontend_implementation / HTMLDisplay.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  21.7 KB  |  615 lines

  1. import app
  2. import threading
  3. import socket
  4. import re
  5. import resource
  6. import xhtmltools
  7. import time
  8. import errno
  9. import os
  10. import config
  11. import util
  12. from util import quoteJS
  13.  
  14. def execChromeJS(js):
  15.     """Execute some Javascript in the context of the privileged top-level
  16.     chrome window. Queued and delivered via a HTTP-based event
  17.     mechanism; no return value is recovered."""
  18.     httpServer.classLock.acquire()
  19.     try:
  20.         if httpServer.chromeJavascriptStream:
  21.             print "XULJS: exec %s" % js[0:250]
  22.             httpServer.chromeJavascriptStream.queueChunk("text/plain", js)
  23.         else:
  24.             print "XULJS: queue %s" % js[0:250]
  25.             httpServer.chromeJavascriptQueue.append(js)
  26.     finally:
  27.         httpServer.classLock.release()
  28.  
  29. from frontend_implementation import UIBackendDelegate
  30.  
  31. ###############################################################################
  32. #### HTTP server to deliver pages/events to browsers via XMLHttpRequest    ####
  33. ###############################################################################
  34.  
  35. # document cookie -> (content type, body)
  36. pendingDocuments = {}
  37.  
  38. # The port we're listening on
  39. serverPort = None
  40. lock = threading.RLock() # and a lock protecting it
  41.  
  42. def getDTVPlatformName():
  43.     return "xul"
  44.  
  45. def getServerPort():
  46.     lock.acquire()
  47.     try:
  48.         if serverPort is None:
  49.             # Bring up the server.
  50.             httpListener()
  51.         if serverPort is None:
  52.             raise ValueError, "httpListener didn't set the port"
  53.  
  54.         result = serverPort
  55.     finally:
  56.         lock.release()
  57.  
  58.     return result
  59.  
  60. class httpListener:
  61.     def __init__(self):
  62.         global serverPort
  63.  
  64.         # Create and bind socket; start listening
  65.         self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  66.         self.socket.settimeout(None)
  67.         self.socket.bind( ('127.0.0.1', 0) )
  68.         (myAddr, myPort) = self.socket.getsockname()
  69.         print "httpListener: Listening on %s %s" % (myAddr, myPort)
  70.         if serverPort:
  71.             raise RuntimeError, "Only one httpListener allowed, please"
  72.         serverPort = myPort
  73.         self.socket.listen(63)
  74.  
  75.         # Kick off the accept loop in a new thread
  76.         thread = threading.Thread(target = self.acceptThread, \
  77.                                   name = "httpListener accept thread")
  78.         thread.setDaemon(True)
  79.         thread.start()
  80.  
  81.     def acceptThread(self):
  82.         while True:
  83.             (conn, address) = self.socket.accept()
  84.             conn.settimeout(None)
  85.             httpServer(conn)
  86.  
  87. class httpServer:
  88.  
  89.     classLock = threading.RLock()
  90.     chromeJavascriptStream = None
  91.     chromeJavascriptQueue = []
  92.     reqNum = 0
  93.  
  94.     def __init__(self, socket):
  95.         self.socket = socket
  96.         self.file = socket.makefile("rb")
  97.         self.isChunked = False
  98.         self.chunkQueue = []
  99.         self.reqNum = None
  100.         self.cond = threading.Condition()
  101.  
  102.         # NEEDS: more convincing random ID
  103.         self.boundary = "DTVDTVDTVDTVDTVDTV%s" % (str(id(self)))
  104.  
  105.         # Kick off a thread that can block waiting for a request to be
  106.         # received
  107.         self.thread = threading.Thread(target = self.requestThread, \
  108.                                        name = "httpServer -- reading request")
  109.         self.thread.setDaemon(True)
  110.         self.thread.start()
  111.  
  112.     def incReqNum(self):
  113.         ret = -1
  114.         httpServer.classLock.acquire()
  115.         try:
  116.             httpServer.reqNum += 1
  117.             ret = httpServer.reqNum
  118.         finally:
  119.             httpServer.classLock.release()
  120.         return ret
  121.  
  122.     def requestThread(self):
  123.         request = None
  124.         
  125.         try:
  126.             try:
  127.                 request = self.file.readline()
  128.                 match = re.match(r"^([^ ]+) +([^ ]+)", request)
  129.                 if request == '':
  130.                     # an empty string indicates we've hit EOS already, don't
  131.                     # bother sending anything back.
  132.                     print "WARNING: empty HTTP request"
  133.                 elif not match:
  134.                     print "WARNING: Malformed HTTP request: %r" % request
  135.                     self.sendBadRequestResponse()
  136.                 else:
  137.                     method = match.group(1)
  138.                     path = match.group(2)
  139.                     self.reqNum = self.incReqNum()
  140.                     self.thread.setName("httpServer [%d] -- %s" % \
  141.                                         (self.reqNum, path))
  142.  
  143.                     self.handleRequest(method, path)
  144.  
  145.             # In handling exceptions, remember that reqNum can be None if
  146.             # the initial readline failed -- so use %s, never %d, when
  147.             # printing it.
  148.             except socket.error, (code, description):
  149.                 if code == errno.ECONNABORTED or \
  150.                         code == errno.ECONNRESET:
  151.                     # Normal: Mozilla was just being abrupt
  152.                     print "[%s] Ignoring remote or network error '%s'" % \
  153.                         (self.reqNum, description)
  154.                     return
  155.                 else:
  156.                     details = "Closing socket; request was [%s] %s" % \
  157.                         (self.reqNum, request)
  158.                     util.failedExn("when answering a request",
  159.                                    details = details)
  160.             except:
  161.                 details = "Closing socket; request was [%s] %s" % \
  162.                     (self.reqNum, request)
  163.                 util.failedExn("when answering a request", details = details)
  164.  
  165.         finally:
  166.             self.socket.close()
  167.             self.file.close()
  168.  
  169.         # Thread exits at this point
  170.  
  171.     def handleRequest(self, method, path):
  172.         if not method == 'GET':
  173.             raise ValueError, "Only GET is supported"
  174.  
  175.         ## Mutator stream ##
  176.         match = re.match("^/dtv/mutators/(.*)", path)
  177.         if match:
  178.             cookie = match.group(1)
  179.             print "[%s @%s] Events" % (self.reqNum, cookie)
  180.  
  181.             self.beginSendingChunks()
  182.             HTMLDisplay.setMutationOutput(cookie, self)
  183.             self.runChunkPump()
  184.             return
  185.  
  186.         ## Chrome-context Javascript stream ##
  187.         match = re.match("^/dtv/xuljs", path)
  188.         if match:
  189.             print "[%s] XULJS" % (self.reqNum)
  190.  
  191.             httpServer.classLock.acquire()
  192.             try:
  193.                 if httpServer.chromeJavascriptStream:
  194.                     raise RuntimeError, \
  195.                         "There can't be two xuljs's (%d)" % self.reqNum
  196.  
  197.                 self.beginSendingChunks()
  198.                 for a in httpServer.chromeJavascriptQueue:
  199.                     print "XULJS: flush %s" % a[0:250]
  200.                     self.queueChunk("text/plain", a)
  201.                 httpServer.chromeJavascriptQueue = []
  202.                 httpServer.chromeJavascriptStream = self
  203.             finally:
  204.                 httpServer.classLock.release()
  205.  
  206.             self.runChunkPump()
  207.             return
  208.  
  209.         ## Chrome-context Preferences Javascript stream ##
  210.         match = re.match("^/dtv/prefjs", path)
  211.         if match:
  212.             print "[%s] PREFJS" % (self.reqNum)
  213.  
  214.             self.beginSendingChunks()
  215.             if (config.get(config.RUN_AT_STARTUP)):
  216.                 self.queueChunk("text/plain", "setRunAtStartup(true);")
  217.             else:
  218.                 self.queueChunk("text/plain", "setRunAtStartup(false);")
  219.             checkEvery = config.get(config.CHECK_CHANNELS_EVERY_X_MN)
  220.             self.queueChunk("text/plain", "setCheckEvery('%s');" % checkEvery)
  221.             speed = config.get(config.UPSTREAM_LIMIT_IN_KBS)
  222.             self.queueChunk("text/plain", "setMaxUpstream(%s);" % speed)
  223.  
  224.             if (config.get(config.LIMIT_UPSTREAM)):
  225.                 self.queueChunk("text/plain", "setLimitUpstream(true);")
  226.             else:
  227.                 self.queueChunk("text/plain", "setLimitUpstream(false);")
  228.  
  229.             min = config.get(config.PRESERVE_X_GB_FREE)
  230.             self.queueChunk("text/plain", "setMinDiskSpace(%s);" % min)
  231.             if (config.get(config.PRESERVE_DISK_SPACE)):
  232.                 self.queueChunk("text/plain", "setHasMinDiskSpace(true);")
  233.             else:
  234.                 self.queueChunk("text/plain", "setHasMinDiskSpace(false);")
  235.  
  236.             expire = config.get(config.EXPIRE_AFTER_X_DAYS)
  237.             self.queueChunk("text/plain", "setExpire('%s');" % expire)
  238.  
  239.  
  240.             self.runChunkPump()
  241.             return
  242.  
  243.         ## Initial HTML ##
  244.         match = re.match("^/dtv/document/(.*)", path)
  245.         if match:
  246.             cookie = match.group(1)
  247.             print "[%s @%s] Initial HTML" % (self.reqNum, cookie)
  248.  
  249.             if not cookie in pendingDocuments:
  250.                 print "bad document request %s to HTMLDisplay server %d" % \
  251.                     (cookie, self.reqNum)
  252.                 self.sendNotFoundResponse(path)
  253.                 return
  254.  
  255.             (contentType, body) = pendingDocuments[cookie]
  256.             del pendingDocuments[cookie]
  257.  
  258.             self.sendDocumentAndClose(contentType, body)
  259.             return
  260.  
  261.         ## Action ## 
  262.         match = re.match(r"^/dtv/action/([^?]*)\?(.*)", path)
  263.         if match:
  264.             cookie = match.group(1)
  265.             url = match.group(2)
  266.             print "[%s @%s] Action: %s" % (self.reqNum, cookie, url)
  267.  
  268.         HTMLDisplay.dispatchEventByCookie(cookie, url)
  269.             self.sendDocumentAndClose("text/plain", "")
  270.             return
  271.  
  272.         ## Returns result from UI Backend Delegate call ##
  273.  
  274.         # If we find that in the future the web server is being
  275.         # littered with lots of URLs specific to the frontend, we may
  276.         # want to make a more general system for registering python functions
  277.         # that can be called by XUL
  278.  
  279.         match = re.match(r"^/dtv/delegateresult/([^?]*)\?(.*)", path)
  280.         if match:
  281.             cookie = match.group(1)
  282.             url = match.group(2)
  283.             print "[%s @%s] UIBackendDelegate: %s" % (self.reqNum, cookie, url)
  284.  
  285.         UIBackendDelegate.dispatchResultByCookie(cookie, url)
  286.             self.sendDocumentAndClose("text/plain", "")
  287.             return
  288.  
  289.         ## Channel guide API ##
  290.         match = re.match(r"^/dtv/dtvapi/([^?]+)\?(.*)", path)
  291.         if match:
  292.             # NEEDS: it may be necessary to encode the url parameter
  293.             # in JS, and decode it here. I'm not super-clear on the
  294.             # circumstances (if any) under which Mozilla would treat
  295.             # the query string as other than opaque bytes.
  296.             action = match.group(1)
  297.             parameter = match.group(2)
  298.             print "[%s] DTVAPI: action %s, parameter %s" % (self.reqNum, \
  299.                                                             action, parameter)
  300.  
  301.             if action == 'addChannel':
  302.                 app.Controller.instance.addFeed(parameter)
  303.             elif action == 'goToChannel':
  304.                 app.Controller.instance.selectFeed(parameter)
  305.             else:
  306.                 print "WARNING: ignored bad DTVAPI request '%s'" % request
  307.  
  308.             self.sendDocumentAndClose("text/plain", "")
  309.             return
  310.  
  311.         ## Resource file ##
  312.         match = re.match("^/dtv/resource/(.*)", path)
  313.         if match:
  314.             relativePath = match.group(1)
  315.             print "[%s] Resource: %s" % (self.reqNum, relativePath)
  316.  
  317.             # Sanity-check the path. We're very liberal about
  318.             # rejecting paths -- anything that has two consecutive
  319.             # periods is thrown out, and a quoting character is
  320.             # optionally allowed between them (even though I don't
  321.             # thing this would actally help.)
  322.             if re.search(r"\.\\?\.", relativePath):
  323.                 print "[%s] Rejecting stupid-looking path" % (self.reqNum, )
  324.                 return
  325.  
  326.             # Open the file.
  327.             fullPath = resource.path(relativePath)
  328.             data = open(fullPath,'rb').read()
  329.  
  330.             # Guess the content-type.
  331.             contentType = None
  332.             if re.search(".png$", fullPath):
  333.                 contentType = "image/png"
  334.             elif re.search(".jpg$", fullPath):
  335.                 contentType = "image/jpeg"
  336.             elif re.search(".jpeg$", fullPath):
  337.                 contentType = "image/jpeg"
  338.             elif re.search(".gif$", fullPath):
  339.                 contentType = "image/gif"
  340.             elif re.search(".css$", fullPath):
  341.                 contentType = "text/css"
  342.             elif re.search(".js$", fullPath):
  343.                 contentType = "application/x-javascript"
  344.  
  345.             self.sendDocumentAndClose(contentType, data, cache=True)
  346.             return
  347.  
  348.         ## Fell through - bad URL ##
  349.         # One possible cause is a relative like in a feed like,
  350.         # <IMG src="/ImageOnMyServer/">
  351.         # Another cause is a bug in democracy.  Return a 404 and print a
  352.         # warning to the log.
  353.         print "Request for %s is invalid.  Sending 404 response" % path
  354.         self.sendNotFoundResponse(path)
  355.  
  356.     def sendStatusLine(self, code, reason):
  357.         self.socket.send("HTTP/1.0 %s %s\r\n" % (code, reason))
  358.  
  359.     def sendHeader(self, name, value):
  360.         self.socket.send("%s: %s\r\n" % (name, value))
  361.  
  362.     def finishHeaders(self):
  363.         self.socket.send("\r\n")
  364.  
  365.     def sendBody(self, body):
  366.         self.socket.send(body)
  367.  
  368.  
  369.     def sendDocumentAndClose(self, contentType, data, statusCode="200",
  370.             statusMessage="OK", cache=False):
  371.         self.sendStatusLine(statusCode, statusMessage)
  372.         self.sendHeader("Content-Length", len(data))
  373.         if contentType:
  374.             self.sendHeader("Content-Type", contentType)
  375.         if cache and not 'DTV_DISABLE_CACHE' in os.environ:
  376.             cacheTime = 60*60 # keep it an hour
  377.             thenGMT = time.gmtime(time.time()+cacheTime)
  378.             thenString = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
  379.                                        thenGMT)
  380.             self.sendHeader("Expires", thenString)
  381.         self.finishHeaders()
  382.         self.sendBody(data)
  383.  
  384.     def sendNotFoundResponse(self, path):
  385.         message = """\
  386. <HTML>
  387. <HEAD><TITLE>404 Not Found</TITLE></HEAD>
  388. <BODY>
  389. <H1>Not Found</H1>
  390. <P>The requested URL %s was not found on this server.</P>
  391. </BODY>
  392. </HTML>""" % path
  393.         self.sendDocumentAndClose('text/html', message, "404", "Not Found")
  394.  
  395.     def sendBadRequestResponse(self):
  396.         self.sendStatusLine("400", "Bad Request")
  397.         self.finishHeaders()
  398.  
  399.     def queueChunk(self, mimeType, body):
  400.         self.cond.acquire()
  401.         try:
  402.             if not self.isChunked:
  403.                 raise RuntimeError, \
  404.                     "queueChunk only works on event-based HTTP sessions"
  405.             self.chunkQueue.append((mimeType, body))
  406.             self.cond.notify()
  407.         finally:
  408.             self.cond.release()
  409.  
  410.     def beginSendingChunks(self):
  411.         self.socket.send("""HTTP/1.0 200 OK
  412. Content-Type: multipart/x-mixed-replace;boundary="%s"
  413.  
  414. --%s""" % (self.boundary, self.boundary))
  415.         self.isChunked = True
  416.  
  417.     def runChunkPump(self):
  418.         self.cond.acquire()
  419.         try:
  420.             while True:
  421.                 while len(self.chunkQueue) == 0:
  422.                     self.cond.wait()
  423.             
  424.                 (mimeType, body) = self.chunkQueue[0]
  425.                 self.chunkQueue = self.chunkQueue[1:]
  426.  
  427.                 if body is None:
  428.                     # Request to close the stream (probably because
  429.                     # the display was deselected.)
  430.                     return
  431.  
  432.                 self.cond.release()
  433.                 try:
  434.                     try:
  435.                         self.socket.send("Content-type: %s\r\n\r\n%s\r\n--%s" \
  436.                                          % (mimeType, body, self.boundary))
  437.                     except socket.error, (code, description):
  438.                         if code == errno.ECONNABORTED or \
  439.                                 code == errno.ECONNRESET:
  440.                             print "[%d] Events end with remote error '%s'" % \
  441.                                 (self.reqNum, description)
  442.                             return
  443.                         else:
  444.                             raise
  445.                 finally:
  446.                     self.cond.acquire()
  447.  
  448.         finally:
  449.             self.cond.release()
  450.             self.socket.close()
  451.  
  452. ###############################################################################
  453. #### Channel guide support                                                 ####
  454. ###############################################################################
  455.  
  456. # These are used by the channel guide. See ChannelGuideToDtvApi in the
  457. # Trac wiki for the full writeup.
  458.  
  459. def getDTVAPICookie():
  460.     return str(getServerPort())
  461.  
  462. def getDTVAPIURL():
  463.     return "http://127.0.0.1:%s/dtv/resource/dtvapi.js" % getServerPort()
  464.  
  465. ###############################################################################
  466. #### HTML display                                                          ####
  467. ###############################################################################
  468.  
  469. def _genMutator(name):
  470.     """Internal: Generates a method that causes the javascript function with
  471. the given name to be called with the arguments passed to the method. Each
  472. argument will be turned into a string and quoted according to Javascript's
  473. requirements. When the method is called, it returns immediately, and the
  474. request goes in a queue."""
  475.     def mutatorFunc(self, *args):
  476.         args = ','.join(['"%s"' % quoteJS(a) for a in args])
  477.         command = "%s(%s);" % (name, args)
  478.         command = xhtmltools.toUTF8Bytes(command)         
  479.         self.outputChunk("text/plain", command)
  480.  
  481.     return mutatorFunc
  482.  
  483. class HTMLDisplay (app.Display):
  484.     "Selectable Display that shows a HTML document."
  485.  
  486.     def __init__(self, html, existingView=None, frameHint=None, areaHint=None,
  487.                  baseURL=None):
  488.         """'html' is the initial contents of the display, as a string.
  489.         Remaining arguments are ignored."""
  490.  
  491.         html=xhtmltools.toUTF8Bytes(html)
  492.  
  493.         if baseURL is not None:
  494.             # This is something the Mac port uses. Complain about that.
  495.             print "WARNING: HTMLDisplay ignoring baseURL '%s'" % baseURL
  496.  
  497.         app.Display.__init__(self)
  498.  
  499.         # Save the HTML so the server can find it
  500.         pendingDocuments[self.getEventCookie()] = ("text/html", html)
  501.  
  502.     self.lock = threading.RLock()
  503.         self.mutationOutput = None
  504.         self.queue = []
  505.  
  506.     def getURL(self):
  507.         """Return the URL to load to see this document."""
  508.         return "http://127.0.0.1:%s/dtv/document/%s" % \
  509.             (self.getServerPort(), self.getEventCookie())
  510.  
  511.     # The mutation functions.
  512.     addItemAtEnd = _genMutator('addItemAtEnd')
  513.     addItemBefore = _genMutator('addItemBefore')
  514.     removeItem = _genMutator('removeItem')
  515.     changeItem = _genMutator('changeItem')
  516.     hideItem = _genMutator('hideItem')
  517.     showItem = _genMutator('showItem')
  518.  
  519.     def outputChunk(self, mimeType, body):
  520.         self.lock.acquire()
  521.         try:
  522.             if self.mutationOutput:
  523.                 self.mutationOutput.queueChunk(mimeType, body)
  524.             else:
  525.                 self.queue.append(body)
  526.         finally:
  527.             self.lock.release()
  528.  
  529.     def onDeselected(self, frame):
  530.         # Close the event stream.
  531.         self.outputChunk(None, None)
  532.  
  533.     ### Concerning dispatching events via context cookies ###
  534.  
  535.     cookieToInstanceMap = {}
  536.  
  537.     # NEEDS: security audit: do we need to make cookies difficult to
  538.     # predict?
  539.     def getEventCookie(self):
  540.     # Can't do this initialization in constructor, because of
  541.     # circular dependency between HTMLDisplay constructor and
  542.     # derived TemplateDisplay constructor. (You need the initial
  543.     # HTML to create the HTMLDisplay, but you need the eventCookie
  544.     # to make the initial HTML.) NEEDS: wish there was a way to
  545.     # put a mutex around this. Is safe in the current
  546.     # implementation, though, because getEventCookie is always
  547.     # called first from the TemplateDisplay constructor.
  548.     if hasattr(self, 'eventCookie'):
  549.         return self.eventCookie
  550.  
  551.     # Create cookie and add this instance to the instance cookie
  552.     # lookup table
  553.     self.eventCookie = str(id(self))
  554.     HTMLDisplay.cookieToInstanceMap[self.eventCookie] = self
  555.  
  556.     return self.eventCookie
  557.  
  558.     def getDTVPlatformName(self):
  559.         return getDTVPlatformName()
  560.  
  561.     def getServerPort(self):
  562.         port = getServerPort()
  563.         return port
  564.  
  565.     @classmethod
  566.     def dispatchEventByCookie(klass, eventCookie, eventURL):
  567.         thread = threading.Thread(target=lambda : klass.cookieToInstanceMap[eventCookie].onURLLoad(eventURL))
  568.         thread.setName("dispatchEvent -- %s" % eventURL)
  569.         thread.setDaemon(False)
  570.         thread.start()
  571.  
  572.     def onURLLoad(self, url):
  573.         """Called when this HTML browser attempts to load a URL (either
  574.         through user action or Javascript.) The URL is provided as a
  575.         string. Return true to allow the URL to load, or false to cancel
  576.         the load (for example, because it was a magic URL that marks
  577.         an item to be downloaded.) Implementation in HTMLDisplay always
  578.         returns true; override in a subclass to implement special
  579.         behavior."""
  580.         # For overriding
  581.         return True
  582.  
  583.     @classmethod
  584.     def setMutationOutput(klass, eventCookie, htmlServer):
  585.     self = klass.cookieToInstanceMap[eventCookie]
  586.         if self.mutationOutput:
  587.             raise RuntimeError, "HTMLDisplay already has its htmlServer"
  588.  
  589.         self.lock.acquire()
  590.         try:
  591.             self.mutationOutput = htmlServer
  592.             for q in self.queue:
  593.                 self.mutationOutput.queueChunk('text/plain', q)
  594.             self.queue = []
  595.         finally:
  596.             self.lock.release()
  597.  
  598.     ### Concerning destruction ###
  599.  
  600.     def unlink(self):
  601.     self.lock.acquire()
  602.     try:
  603.         if self.eventCookie in HTMLDisplay.cookieToInstanceMap:
  604.         del HTMLDisplay.cookieToInstanceMap[self.eventCookie]
  605.             if self.eventCookie in pendingDocuments:
  606.                 del pendingDocuments[self.eventCookie]
  607.     finally:
  608.         self.lock.release()
  609.  
  610.     def __del__(self):
  611.         self.unlink()
  612.  
  613. ###############################################################################
  614. ###############################################################################
  615.